/*
* Copyright 2015 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package io.netty.handler.codec.http.router;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.QueryStringDecoder;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
/**
* Router that contains information about both route matching orders and
* HTTP request methods.
*
* Routes are devided into 3 sections: "first", "last", and "other".
* Routes in "first" are matched first, then in "other", then in "last".
*
* <h3>Create router</h3>
*
* Route targets can be any type. In the below example, targets are classes:
*
* <pre>
* {@code
* Router<Class> router = new Router<Class>()
* .GET ("/articles", IndexHandler.class)
* .GET ("/articles/:id", ShowHandler.class)
* .POST ("/articles", CreateHandler.class)
* .GET ("/download/:*", DownloadHandler.class) // ":*" must be the last token
* .GET_FIRST("/articles/new", NewHandler.class); // This will be matched first
* }
* </pre>
*
* Slashes at both ends are ignored. These are the same:
*
* <pre>
* {@code
* router.GET("articles", IndexHandler.class);
* router.GET("/articles", IndexHandler.class);
* router.GET("/articles/", IndexHandler.class);
* }
* </pre>
*
* You can remove routes by target or by path:
*
* <pre>
* {@code
* router.removeTarget(IndexHandler.class);
* router.removePath("/articles");
* }
* </pre>
*
* <h3>Match with request method and URI</h3>
*
* Use {@link #route(HttpMethod, String)}.
*
* From the {@link RouteResult} you can extract params embedded in
* the path and from the query part of the request URI.
*
* <h3>404 Not Found target</h3>
*
* Use {@link #notFound(Object)}. It will be used as the target
* when there's no match.
*
* <pre>
* router.notFound(My404Handler.class);
* </pre>
*
* <h3>Create reverse route</h3>
*
* Use {@link #path(HttpMethod, Object, Object...)} or {@link #path(Object, Object...)}:
*
* <pre>
* {@code
* router.path(HttpMethod.GET, IndexHandler.class);
* // Returns "/articles"
* }
* </pre>
*
* You can skip HTTP method if there's no confusion:
*
* <pre>
* {@code
* router.path(CreateHandler.class);
* // Also returns "/articles"
* }
* </pre>
*
* You can specify params as map:
*
* <pre>
* {@code
* // Things in params will be converted to String
* Map<Object, Object> params = new HashMap<Object, Object>();
* params.put("id", 123);
* router.path(ShowHandler.class, params);
* // Returns "/articles/123"
* }
* </pre>
*
* Convenient way to specify params:
*
* <pre>
* {@code
* router.path(ShowHandler.class, "id", 123);
* // Returns "/articles/123"
* }
* </pre>
*
* <h3>Allowed methods</h3>
*
* If you want to implement
* <a href="https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Request_methods">OPTIONS</a>
* or
* <a href="https://en.wikipedia.org/wiki/Cross-origin_resource_sharing">CORS</a>,
* you can use {@link #allowedMethods(String)}.
*
* For {@code OPTIONS *}, use {@link #allAllowedMethods()}.
*/
public class Router<T> {
private final Map<HttpMethod, MethodlessRouter<T>> routers =
new HashMap<HttpMethod, MethodlessRouter<T>>();
private final MethodlessRouter<T> anyMethodRouter =
new MethodlessRouter<T>();
private T notFound;
//--------------------------------------------------------------------------
// Design decision:
// We do not allow access to routers and anyMethodRouter, because we don't
// want to expose MethodlessRouter, OrderlessRouter, and Path.
// Exposing those will complicate the use of this package.
/**
* Returns the fallback target for use when there's no match at
* {@link #route(HttpMethod, String)}.
*/
public T notFound() {
return notFound;
}
/** Returns the number of routes in this router. */
public int size() {
int ret = anyMethodRouter.size();
for (MethodlessRouter<T> router : routers.values()) {
ret += router.size();
}
return ret;
}
//--------------------------------------------------------------------------
/**
* Add route to the "first" section.
*
* A path can only point to one target. This method does nothing if the path
* has already been added.
*/
public Router addRouteFirst(HttpMethod method, String path, T target) {
getMethodlessRouter(method).addRouteFirst(path, target);
return this;
}
/**
* Add route to the "other" section.
*
* A path can only point to one target. This method does nothing if the path
* has already been added.
*/
public Router addRoute(HttpMethod method, String path, T target) {
getMethodlessRouter(method).addRoute(path, target);
return this;
}
/**
* Add route to the "last" section.
*
* A path can only point to one target. This method does nothing if the path
* has already been added.
*/
public Router addRouteLast(HttpMethod method, String path, T target) {
getMethodlessRouter(method).addRouteLast(path, target);
return this;
}
/**
* Sets the fallback target for use when there's no match at
* {@link #route(HttpMethod, String)}.
*/
public Router notFound(T target) {
this.notFound = target;
return this;
}
private MethodlessRouter<T> getMethodlessRouter(HttpMethod method) {
if (method == null) {
return anyMethodRouter;
}
MethodlessRouter<T> r = routers.get(method);
if (r == null) {
r = new MethodlessRouter<T>();
routers.put(method, r);
}
return r;
}
//--------------------------------------------------------------------------
/** Removes the route specified by the path. */
public void removePath(String path) {
for (MethodlessRouter<T> r : routers.values()) {
r.removePath(path);
}
anyMethodRouter.removePath(path);
}
/** Removes all routes leading to the target. */
public void removeTarget(T target) {
for (MethodlessRouter<T> r : routers.values()) {
r.removeTarget(target);
}
anyMethodRouter.removeTarget(target);
}
//--------------------------------------------------------------------------
/**
* If there's no match, returns the result with {@link #notFound(Object) notFound}
* as the target if it is set, otherwise returns {@code null}.
*/
public RouteResult<T> route(HttpMethod method, String uri) {
QueryStringDecoder decoder = new QueryStringDecoder(uri);
String[] tokens = Path.removeSlashesAtBothEnds(decoder.path()).split("/");
MethodlessRouter<T> router = routers.get(method);
if (router == null) {
router = anyMethodRouter;
}
RouteResult<T> ret = router.route(tokens);
if (ret != null) {
return new RouteResult(ret.target(), ret.pathParams(), decoder.parameters());
}
if (router != anyMethodRouter) {
ret = anyMethodRouter.route(tokens);
if (ret != null) {
return new RouteResult(ret.target(), ret.pathParams(), decoder.parameters());
}
}
if (notFound != null) {
// Return mutable map to be consistent, instead of
// Collections.<String, String>emptyMap()
return new RouteResult<T>(notFound, new HashMap<String, String>(), decoder.parameters());
}
return null;
}
//--------------------------------------------------------------------------
// For implementing OPTIONS and CORS.
/**
* Returns allowed methods for a specific URI.
*
* For {@code OPTIONS *}, use {@link #allAllowedMethods()} instead of this method.
*/
public Set<HttpMethod> allowedMethods(String uri) {
QueryStringDecoder decoder = new QueryStringDecoder(uri);
String[] tokens = Path.removeSlashesAtBothEnds(decoder.path()).split("/");
if (anyMethodRouter.anyMatched(tokens)) {
return allAllowedMethods();
}
Set<HttpMethod> ret = new HashSet<HttpMethod>(routers.size());
for (Map.Entry<HttpMethod, MethodlessRouter<T>> entry : routers.entrySet()) {
MethodlessRouter<T> router = entry.getValue();
if (router.anyMatched(tokens)) {
HttpMethod method = entry.getKey();
ret.add(method);
}
}
return ret;
}
/** Returns all methods that this router handles. For {@code OPTIONS *}. */
public Set<HttpMethod> allAllowedMethods() {
if (anyMethodRouter.size() > 0) {
Set<HttpMethod> ret = new HashSet<HttpMethod>(9);
ret.add(HttpMethod.CONNECT);
ret.add(HttpMethod.DELETE);
ret.add(HttpMethod.GET);
ret.add(HttpMethod.HEAD);
ret.add(HttpMethod.OPTIONS);
ret.add(HttpMethod.PATCH);
ret.add(HttpMethod.POST);
ret.add(HttpMethod.PUT);
ret.add(HttpMethod.TRACE);
return ret;
} else {
return new HashSet<HttpMethod>(routers.keySet());
}
}
//--------------------------------------------------------------------------
/**
* Given a target and params, this method tries to do the reverse routing
* and returns the path.
*
* The params are put to placeholders in the path.
* The params can be a map of {@code placeholder name -> value}
* or ordered values. If a param doesn't have a placeholder, it will be put
* to the query part of the path.
*
* @return {@code null} if there's no match
*/
public String path(HttpMethod method, T target, Object... params) {
MethodlessRouter<T> router = (method == null)? anyMethodRouter : routers.get(method);
// Fallback to anyMethodRouter if no router is found for the method
if (router == null) {
router = anyMethodRouter;
}
String ret = router.path(target, params);
if (ret != null) {
return ret;
}
// Fallback to anyMethodRouter if the router was not anyMethodRouter and no path is found
return (router != anyMethodRouter)? anyMethodRouter.path(target, params) : null;
}
/**
* Given a target and params, this method tries to do the reverse routing
* and returns the path.
*
* The params are put to placeholders in the path.
* The params can be a map of {@code placeholder name -> value}
* or ordered values. If a param doesn't have a placeholder, it will be put
* to the query part of the path.
*
* @return {@code null} if there's no match
*/
public String path(T target, Object... params) {
Collection<MethodlessRouter<T>> rs = routers.values();
for (MethodlessRouter<T> r : rs) {
String ret = r.path(target, params);
if (ret != null) {
return ret;
}
}
return anyMethodRouter.path(target, params);
}
//--------------------------------------------------------------------------
/** Returns visualized routing rules. */
@Override
public String toString() {
// Step 1/2: Dump routers and anyMethodRouter in order
int numRoutes = size();
List<String> methods = new ArrayList<String>(numRoutes);
List<String> paths = new ArrayList<String>(numRoutes);
List<String> targets = new ArrayList<String>(numRoutes);
// For router
for (Entry<HttpMethod, MethodlessRouter<T>> e : routers.entrySet()) {
HttpMethod method = e.getKey();
MethodlessRouter<T> router = e.getValue();
aggregateRoutes(method.toString(), router.first().routes(), methods, paths, targets);
aggregateRoutes(method.toString(), router.other().routes(), methods, paths, targets);
aggregateRoutes(method.toString(), router.last() .routes(), methods, paths, targets);
}
// For anyMethodRouter
aggregateRoutes("*", anyMethodRouter.first().routes(), methods, paths, targets);
aggregateRoutes("*", anyMethodRouter.other().routes(), methods, paths, targets);
aggregateRoutes("*", anyMethodRouter.last() .routes(), methods, paths, targets);
// For notFound
if (notFound != null) {
methods.add("*");
paths .add("*");
targets.add(targetToString(notFound));
}
// Step 2/2: Format the List into aligned columns: <method> <path> <target>
int maxLengthMethod = maxLength(methods);
int maxLengthPath = maxLength(paths);
String format = "%-" + maxLengthMethod + "s %-" + maxLengthPath + "s %s\n";
int initialCapacity = (maxLengthMethod + 1 + maxLengthPath + 1 + 20) * methods.size();
StringBuilder b = new StringBuilder(initialCapacity);
for (int i = 0; i < methods.size(); i++) {
String method = methods.get(i);
String path = paths .get(i);
String target = targets.get(i);
b.append(String.format(format, method, path, target));
}
return b.toString();
}
/** Helper for toString */
private static <T> void aggregateRoutes(
String method, Map<Path, T> routes,
List<String> accMethods, List<String> accPaths, List<String> accTargets) {
for (Map.Entry<Path, T> entry : routes.entrySet()) {
accMethods.add(method);
accPaths .add("/" + entry.getKey().path());
accTargets.add(targetToString(entry.getValue()));
}
}
/** Helper for toString */
private static int maxLength(List<String> coll) {
int max = 0;
for (String e : coll) {
int length = e.length();
if (length > max) {
max = length;
}
}
return max;
}
/**
* Helper for toString; for example, returns
* "io.netty.example.http.router.HttpRouterServerHandler" instead of
* "class io.netty.example.http.router.HttpRouterServerHandler"
*/
private static String targetToString(Object target) {
if (target instanceof Class) {
String className = ((Class<?>) target).getName();
return className;
} else {
return target.toString();
}
}
//--------------------------------------------------------------------------
public Router CONNECT(String path, T target) {
return addRoute(HttpMethod.CONNECT, path, target);
}
public Router DELETE(String path, T target) {
return addRoute(HttpMethod.DELETE, path, target);
}
public Router GET(String path, T target) {
return addRoute(HttpMethod.GET, path, target);
}
public Router HEAD(String path, T target) {
return addRoute(HttpMethod.HEAD, path, target);
}
public Router OPTIONS(String path, T target) {
return addRoute(HttpMethod.OPTIONS, path, target);
}
public Router PATCH(String path, T target) {
return addRoute(HttpMethod.PATCH, path, target);
}
public Router POST(String path, T target) {
return addRoute(HttpMethod.POST, path, target);
}
public Router PUT(String path, T target) {
return addRoute(HttpMethod.PUT, path, target);
}
public Router TRACE(String path, T target) {
return addRoute(HttpMethod.TRACE, path, target);
}
public Router ANY(String path, T target) {
return addRoute(null, path, target);
}
//--------------------------------------------------------------------------
public Router CONNECT_FIRST(String path, T target) {
return addRouteFirst(HttpMethod.CONNECT, path, target);
}
public Router DELETE_FIRST(String path, T target) {
return addRouteFirst(HttpMethod.DELETE, path, target);
}
public Router GET_FIRST(String path, T target) {
return addRouteFirst(HttpMethod.GET, path, target);
}
public Router HEAD_FIRST(String path, T target) {
return addRouteFirst(HttpMethod.HEAD, path, target);
}
public Router OPTIONS_FIRST(String path, T target) {
return addRouteFirst(HttpMethod.OPTIONS, path, target);
}
public Router PATCH_FIRST(String path, T target) {
return addRouteFirst(HttpMethod.PATCH, path, target);
}
public Router POST_FIRST(String path, T target) {
return addRouteFirst(HttpMethod.POST, path, target);
}
public Router PUT_FIRST(String path, T target) {
return addRouteFirst(HttpMethod.PUT, path, target);
}
public Router TRACE_FIRST(String path, T target) {
return addRouteFirst(HttpMethod.TRACE, path, target);
}
public Router ANY_FIRST(String path, T target) {
return addRouteFirst(null, path, target);
}
//--------------------------------------------------------------------------
public Router CONNECT_LAST(String path, T target) {
return addRouteLast(HttpMethod.CONNECT, path, target);
}
public Router DELETE_LAST(String path, T target) {
return addRouteLast(HttpMethod.DELETE, path, target);
}
public Router GET_LAST(String path, T target) {
return addRouteLast(HttpMethod.GET, path, target);
}
public Router HEAD_LAST(String path, T target) {
return addRouteLast(HttpMethod.HEAD, path, target);
}
public Router OPTIONS_LAST(String path, T target) {
return addRouteLast(HttpMethod.OPTIONS, path, target);
}
public Router PATCH_LAST(String path, T target) {
return addRouteLast(HttpMethod.PATCH, path, target);
}
public Router POST_LAST(String path, T target) {
return addRouteLast(HttpMethod.POST, path, target);
}
public Router PUT_LAST(String path, T target) {
return addRouteLast(HttpMethod.PUT, path, target);
}
public Router TRACE_LAST(String path, T target) {
return addRouteLast(HttpMethod.TRACE, path, target);
}
public Router ANY_LAST(String path, T target) {
return addRouteLast(null, path, target);
}
}